package by.gamedev.site.client.app.place;

import java.util.Set;

import com.google.gwt.activity.shared.Activity;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.place.shared.PlaceController;
import com.google.gwt.requestfactory.client.RequestFactoryEditorDriver;
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.EntityProxyId;
import com.google.gwt.requestfactory.shared.Receiver;
import com.google.gwt.requestfactory.shared.RequestContext;
import com.google.gwt.requestfactory.shared.ServerFailure;
import com.google.gwt.requestfactory.shared.Violation;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.AcceptsOneWidget;

/**
 * Abstract activity for editing a record. Subclasses must provide access to the request that will be fired when Save is clicked.
 * <p>
 * Instances are not reusable. Once an activity is stoped, it cannot be restarted.
 * 
 * @param <P>
 *            the type of Proxy being edited
 */
public abstract class AbstractProxyEditActivity<P extends EntityProxy> implements Activity, ProxyEditView.Delegate {

    protected final ProxyEditView<P, ?> view;
    private final PlaceController       placeController;

    RequestFactoryEditorDriver<P, ?>    editorDriver;
    private boolean                     waiting;

    public AbstractProxyEditActivity(final ProxyEditView<P, ?> view, final PlaceController placeController) {
        this.view = view;
        this.placeController = placeController;
    }

    @Override
    public void cancelClicked() {
        final String unsavedChangesWarning = mayStop();
        if(unsavedChangesWarning == null || Window.confirm(unsavedChangesWarning) ) {
            editorDriver = null;
            exit(false);
        }
    }

    @Override
    public String mayStop() {
        if(isWaiting() || changed() ) {
            return "Are you sure you want to abandon your changes?";
        }

        return null;
    }

    @Override
    public void onCancel() {
        onStop();
    }

    @Override
    public void onStop() {
        view.setDelegate(null);
        editorDriver = null;
    }

    @Override
    public void saveClicked() {
        if(!changed() ) {
            return;
        }

        setWaiting(true);
        editorDriver.flush().fire(new Receiver<Void>() {
            /*
             * Callbacks do nothing if editorDriver is null, we were stopped in midflight
             */
            @Override
            public void onFailure(final ServerFailure error) {
                if(editorDriver != null ) {
                    setWaiting(false);
                    super.onFailure(error);
                }
            }

            @Override
            public void onSuccess(final Void ignore) {
                if(editorDriver != null ) {
                    // We want no warnings from mayStop, so:

                    // Defeat isChanged check
                    editorDriver = null;

                    // Defeat call-in-flight check
                    setWaiting(false);

                    exit(true);
                }
            }

            @Override
            public void onViolation(final Set<Violation> errors) {
                if(editorDriver != null ) {
                    setWaiting(false);
                    editorDriver.setViolations(errors);
                }
            }
        });
    }

    @Override
    public void start(final AcceptsOneWidget display, final EventBus eventBus) {
        editorDriver = view.createEditorDriver();
        view.setDelegate(this);
        editorDriver.edit(getProxy(), createSaveRequest(getProxy()));
        display.setWidget(view);
    }

    /**
     * Called once to create the appropriate request to save changes.
     * 
     * @return the request context to fire when the save button is clicked
     */
    protected abstract RequestContext createSaveRequest(P proxy);

    /**
     * Called when the user cancels or has successfully saved. This default implementation tells the {@link PlaceController} to show the details of
     * the edited record.
     * 
     * @param saved
     *            true if changes were comitted, false if user canceled
     */
    protected void exit(final boolean saved) {
        placeController.goTo(new ProxyPlace(getProxyId(), ProxyPlace.Operation.DETAILS));
    }

    /**
     * Get the proxy to be edited. Must be mutable, typically via a call to {@link RequestContext#edit(EntityProxy)}, or
     * {@link RequestContext#create(Class)}.
     */
    protected abstract P getProxy();

    @SuppressWarnings("unchecked")
    // id type always matches proxy type
    protected EntityProxyId<P> getProxyId() {
        return (EntityProxyId<P>) getProxy().stableId();
    }

    private boolean changed() {
        return editorDriver != null && editorDriver.flush().isChanged();
    }

    /**
     * @return true if we're waiting for an rpc response.
     */
    private boolean isWaiting() {
        return waiting;
    }

    /**
     * While we are waiting for a response, we cannot poke setters on the proxy (that is, we cannot call editorDriver.flush). So we set the waiting
     * flag to warn ourselves not to, and to disable the view.
     */
    void setWaiting(final boolean wait) {
        this.waiting = wait;
        view.setEnabled(!wait);
    }
}
